iT邦幫忙

2025 iThome 鐵人賽

DAY 8
3
Modern Web

原生元件養成計畫:Web Component系列 第 8

Day 8: Web Component 的事件傳遞 CustomEvent

  • 分享至 

  • xImage
  •  

經過了一週的時間,與前幾篇文章的介紹,對於 Web Component 應該已經有了初步的理解了。
我們從如何透過 templateShadow DOM 來封裝內容,到監聽屬性變更並觸發元件更新,自訂元件也正一步一步的從靜態邁向動態互動中。

那麼下一步,我們要思考:元件該如何和外部世界互動?
元件要怎麼把內部發生的事情告訴外部?事件要怎麼傳遞出去?

自訂事件 CustomEvent


身為一個前端工程師,對於click, input, change 等等事件一定再熟悉不過了。
但也別忘了,在前面我們有提到過 Shadow DOM,它就像是一個密閉的房間,所以裡面的事件不一定能直接傳到外面。
這裡有點像在 Angular,用 @Output + EventEmitter 把事件傳出去。
在 React,透過父元件的 callback,子元件在事件發生時呼叫 callback,把事件往上送。

而在 Web Component,就需要用到 CustomEvent 將你的事件傳遞出去。

如何建立自訂事件並傳遞出去?

先創建事件對象

在我們將事件傳遞出去時,我們必須要先建立監聽觸發事件的對象,如果 listener 還沒綁定,事件就無法被接收到。
我相信 addEventListener 大家應該都很熟悉了!

element.addEventListener('click', () => {});

使用 dispatchEvent 產生要傳遞出去的事件物件

 dispatchEvent(new CustomEvent('自訂事件名稱', { 事件設定 }));
  1. 自訂事件名稱:就像我們自定元件名稱一樣,通常中間會使用 - 做分隔。
  2. detail: 傳遞額外資訊給外部監聽者,外部可以透過 event.detail 讀取。
  3. bubbles: 預設 false,若設成 true,則事件可以一路往上冒泡,讓父層節點也能接到。
  4. composed: 預設 false,若設成 true,則事件可以穿越 Shadow DOM 邊界,傳到元件外部,如果不設定,事件就只會在 Shadow Dom 內部。

完整綁定寫法:

bindCustomEvent() {
  const bindEvent = () => {
    this.dispatchEvent(new CustomEvent('spinner-update', {
      detail: { progress: 50 }, // 事件攜帶的額外資訊
      bubbles: true, // 是否冒泡到外層 DOM
      composed: true // 是否可以穿越 shadow DOM
    }));
  };

  element.addEventListener('click', bindEvent); 
}

在 Shadow DOM 中事件傳遞的注意事項

在 Shadow DOM 中傳遞事件會需要注意以下兩點:

  1. 在外部監聽時,event.target 會被重新定向成你的自訂元素 <cat-spinner>(外部看不到你內部的真實節點)。如果要看完整路徑,需要用 event.composedPath()。
    使用時須確保 this.attachShadow({ mode: 'open' })

composedPath() 此 API 的方法會傳回 Event 事件的路徑,該路徑是一個 Array,其中包含將在其上呼叫監聽器的物件。但是如果影子根是在 ShadowRoot.mode 關閉的情況下建立的,則此方法不包含影子樹中的節點。 - MDN

  1. composed:只有 composed: true 的事件,才會從 shadow tree 冒泡到外層。

替 cat-spinner 加上 overlay,並監聽 overlay 的點擊事件

我們將 cat-spinner 來做延伸,這次替它加上一個 overlay(遮罩)。
當使用者點擊 overlay 時,spinner 會透過 CustomEvent 通知外部 overlay 被點擊了。

加入 overlay 的樣式

button.js

renderTemplate = function(){
  const spinnerTemplate = document.createElement('template');

  spinnerTemplate.innerHTML = `
    <style>
      .overlay {
        position: fixed;
        inset: 0;
        background: rgba(0,0,0,0.4);
        display: flex;
        justify-content: center;
        align-items: center;
      }
      .spinner {
        width: 40px;
        height: 40px;
        border: 4px solid #e8e5e5;
        border-top-color: #926dec;
        border-radius: 50%;
        animation: spin 1s linear infinite;
      }
      @keyframes spin {
        to { 
          transform: rotate(360deg); 
        }
      }
    </style>

    <div class="overlay">
      <div class="spinner"></div>
    </div>
  `;

  return spinnerTemplate;
}

定義事件

class CatSpinner extends HTMLElement {
 constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const cloneNode = this.renderTemplate().content.cloneNode(true);
    shadowRoot.appendChild(cloneNode);
  }

  // 元件被加入 DOM 時綁定事件
  connectedCallback() {
    this.bindOverlayClickEvent();
  }
  
  bindOverlayClickEvent() {
    const overlay = this.shadowRoot.querySelector('.overlay');
    const bindEvent = ()=> {
      // 加入自訂事件
      this.dispatchEvent(new CustomEvent('overlay-click', {
        detail: { overlayClick: true }
        bubbles: true,
        composed: true
      }));
    }

    overlay.addEventListener('click', bindEvent); // 綁定要傳遞出去的事件
  }
}

customElements.define('cat-spinner', CatSpinner);

在外部監聽綁定的事件,並且使用 event.detail 取得事件的附加資訊。

index.html

<body>
  <cat-spinner></cat-spinner>
  
  <script src="spinner.js"></script>
  <script>
    const spinner = document.querySelector('cat-spinner');

    spinner.addEventListener('overlay-click', (event) => {
      console.log('使用者點擊了 overlay!');
      console.log('事件 detail:', event.detail);  // 使用 event.detail 取出 detail 內容
    });
  </script>
</body>

custom event

試試看: 點擊 overlay 後關閉 cat-spinner

在外部監聽綁定的事件,加入移除 cat-spinner 的邏輯,當 <cat-spinner> 從 DOM 被移除時,它的 shadowRoot 也會一起被銷毀。瀏覽器會連同內部的 DOM、事件監聽器一併清理掉,所以不會造成記憶體洩漏。

index.html

<body>
  <cat-spinner></cat-spinner>
  
  <script src="spinner.js"></script>
  <script>
    const spinner = document.querySelector('cat-spinner');

    spinner.addEventListener('overlay-click', (event) => {
      if (event.detail.overlayClick) {
        spinner.remove();
      }
    });
  </script>
</body>

上一篇
Day 7: Web Component 的屬性監聽 Attribute
下一篇
DAY 9: Web Component 的屬性 Properties 與 Getter & Setter
系列文
原生元件養成計畫:Web Component12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言